Skip to content

feat(wallet)!: wire GasFeeController into default initialization - #9527

Merged
sirtimid merged 1 commit into
mainfrom
sirtimid/wire-gas-fee-controller
Jul 22, 2026
Merged

feat(wallet)!: wire GasFeeController into default initialization#9527
sirtimid merged 1 commit into
mainfrom
sirtimid/wire-gas-fee-controller

Conversation

@sirtimid

@sirtimid sirtimid commented Jul 15, 2026

Copy link
Copy Markdown
Member

Explanation

Wires GasFeeController into @metamask/wallet's default initialization set as its own InitializationConfiguration under src/initialization/instances/gas-fee-controller/, mirroring the most recently merged instances (transaction-controller, network-controller).

TransactionController is already wired and its messenger delegates GasFeeController:fetchGasFeeEstimates. Delegation registers a lazy handler, so the wallet boots fine today — but the first transaction flow that needs gas estimates throws A handler for GasFeeController:fetchGasFeeEstimates has not been registered. Wiring GasFeeController is the missing piece that lets a wired TransactionController actually estimate gas.

@metamask/wallet is the shared controller-integration layer that metamask-extension, metamask-mobile, and @metamask/wallet-cli all adopt, so every value that differs between clients is an injectable instanceOptions.gasFeeController slot with a platform-agnostic default — nothing is baked to one client.

Constructor callbacks

GasFeeController takes direct callbacks rather than pure messenger delegation. The instance builds them from the wired NetworkController:

  • getProviderNetworkController:getState (selectedNetworkClientId) then NetworkController:getNetworkClientById(id).provider
  • getCurrentNetworkEIP1559CompatibilityNetworkController:getEIP1559Compatibility (coerced to a defined boolean)

onNetworkDidChange / getChainId are omitted — the constructor already has a messenger-based network-tracking fallback when both are absent (subscribes NetworkController:networkDidChange).

Injectable options + per-environment values

Option Extension Mobile Default (wallet-cli / package)
EIP1559APIEndpoint .../networks/<chain_id>/suggestedGasFees (dev override → gas.uat-api.cx.metamask.io) same prod URL prod URL; injectable
legacyAPIEndpoint ${GAS_API}/networks/<chain_id>/gasPrices same prod URL prod URL; injectable
clientId 'extension' 'mobile' 'cli' (sent as X-Client-Id); injectable
interval 10_000 15_000 controller default 15_000; injectable
getCurrentNetworkLegacyGasAPICompatibility chainId === BSC mainnet || BSC || POLYGON () => false; injectable
getCurrentAccountEIP1559Compatibility () => true omitted () => true; injectable

Initialization ordering

No construction-ordering machinery is needed. GasFeeController was made initialization-order-agnostic upstream (#9569): the constructor no longer eagerly resolves NetworkController (the getProvider / getCurrentNetworkEIP1559Compatibility callbacks resolve it lazily at call time), so it can be constructed before NetworkController. initialize therefore constructs the default set in its natural order, and the dependencies field / orderByDependencies sort that an earlier revision of this PR added were dropped in favor of that upstream refactor.

fetch decision

GasFeeController fetches gas estimates via the global fetch inside @metamask/gas-fee-controller (it has no injectable fetch option). This PR takes path (a): accept the global fetch — no upstream change; works on Node 18+ (wallet-cli daemon), modern browsers, and React Native. The global-fetch usage lives inside the controller package, not inside @metamask/wallet code, and both clients already rely on this today. Adding an injectable fetch option to @metamask/gas-fee-controller is tracked as a possible follow-up if the convention is to be enforced strictly.

Client adoption PRs

References

Related

Checklist

  • I've read the contributing guidelines.
  • I've completed the PR template to the best of my ability.
  • I've included tests if applicable (100% coverage; colocated instance test).
  • I've documented my code using JSDoc format if applicable.
  • I've applied the right labels on the PR (see labeling guidelines). Not required for external contributors.

🤖 Generated with Claude Code


Note

Medium Risk
Breaking API for all Wallet consumers and changes the transaction gas-estimation path, but the integration follows existing controller init patterns and includes dedicated tests.

Overview
Breaking: @metamask/wallet now boots GasFeeController as part of default initialization, so wired TransactionController flows can call GasFeeController:fetchGasFeeEstimates instead of failing with a missing handler.

Consumers must pass instanceOptions.gasFeeController with a required clientId (sent as X-Client-Id to the gas API); other gas settings stay optional with shared defaults (production API URLs, network callbacks built from NetworkController).

@metamask/wallet-cli sets clientId: 'cli' on the daemon wallet. Extension/mobile adoption is expected in separate PRs.

Reviewed by Cursor Bugbot for commit baa4472. Bugbot is set up for automated code reviews on this repo. Configure here.

sirtimid added a commit that referenced this pull request Jul 15, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sirtimid
sirtimid requested a review from a team July 15, 2026 18:13
@sirtimid

Copy link
Copy Markdown
Member Author

@metamaskbot publish-previews

@github-actions

Copy link
Copy Markdown
Contributor

Preview builds have been published. Learn how to use preview builds in other projects.

Expand for full list of packages and versions.
@metamask-previews/account-tree-controller@7.5.5-preview-7ed2770ae
@metamask-previews/accounts-controller@39.0.5-preview-7ed2770ae
@metamask-previews/address-book-controller@7.1.2-preview-7ed2770ae
@metamask-previews/ai-controllers@0.8.0-preview-7ed2770ae
@metamask-previews/analytics-controller@1.2.1-preview-7ed2770ae
@metamask-previews/analytics-data-regulation-controller@0.0.0-preview-7ed2770ae
@metamask-previews/announcement-controller@8.1.0-preview-7ed2770ae
@metamask-previews/app-metadata-controller@2.0.1-preview-7ed2770ae
@metamask-previews/approval-controller@9.0.2-preview-7ed2770ae
@metamask-previews/assets-controller@11.0.0-preview-7ed2770ae
@metamask-previews/assets-controllers@109.4.1-preview-7ed2770ae
@metamask-previews/authenticated-user-storage@3.0.1-preview-7ed2770ae
@metamask-previews/base-controller@9.1.0-preview-7ed2770ae
@metamask-previews/base-data-service@0.1.3-preview-7ed2770ae
@metamask-previews/bitcoin-regtest-up@1.0.0-preview-7ed2770ae
@metamask-previews/bridge-controller@77.5.0-preview-7ed2770ae
@metamask-previews/bridge-status-controller@74.2.0-preview-7ed2770ae
@metamask-previews/build-utils@3.0.4-preview-7ed2770ae
@metamask-previews/chain-agnostic-permission@1.7.0-preview-7ed2770ae
@metamask-previews/chomp-api-service@3.1.0-preview-7ed2770ae
@metamask-previews/claims-controller@0.5.3-preview-7ed2770ae
@metamask-previews/client-controller@1.0.1-preview-7ed2770ae
@metamask-previews/client-utils@1.1.0-preview-7ed2770ae
@metamask-previews/compliance-controller@2.1.0-preview-7ed2770ae
@metamask-previews/composable-controller@12.0.1-preview-7ed2770ae
@metamask-previews/config-registry-controller@0.4.1-preview-7ed2770ae
@metamask-previews/connectivity-controller@0.3.0-preview-7ed2770ae
@metamask-previews/controller-utils@12.3.0-preview-7ed2770ae
@metamask-previews/core-backend@6.5.0-preview-7ed2770ae
@metamask-previews/delegation-controller@3.0.2-preview-7ed2770ae
@metamask-previews/earn-controller@12.2.2-preview-7ed2770ae
@metamask-previews/eip-5792-middleware@3.0.5-preview-7ed2770ae
@metamask-previews/eip-7702-internal-rpc-middleware@0.1.1-preview-7ed2770ae
@metamask-previews/eip1193-permission-middleware@2.0.1-preview-7ed2770ae
@metamask-previews/ens-controller@19.1.5-preview-7ed2770ae
@metamask-previews/eth-block-tracker@15.0.1-preview-7ed2770ae
@metamask-previews/eth-json-rpc-middleware@23.1.3-preview-7ed2770ae
@metamask-previews/eth-json-rpc-provider@6.0.1-preview-7ed2770ae
@metamask-previews/foundryup@1.0.1-preview-7ed2770ae
@metamask-previews/gas-fee-controller@26.2.4-preview-7ed2770ae
@metamask-previews/gator-permissions-controller@4.2.3-preview-7ed2770ae
@metamask-previews/geolocation-controller@0.1.3-preview-7ed2770ae
@metamask-previews/java-tron-up@1.0.0-preview-7ed2770ae
@metamask-previews/json-rpc-engine@10.5.0-preview-7ed2770ae
@metamask-previews/json-rpc-middleware-stream@8.0.8-preview-7ed2770ae
@metamask-previews/keyring-controller@27.1.0-preview-7ed2770ae
@metamask-previews/local-node-utils@1.0.0-preview-7ed2770ae
@metamask-previews/logging-controller@8.0.2-preview-7ed2770ae
@metamask-previews/message-manager@14.1.2-preview-7ed2770ae
@metamask-previews/messenger@2.0.0-preview-7ed2770ae
@metamask-previews/messenger-cli@0.2.0-preview-7ed2770ae
@metamask-previews/money-account-api-data-service@0.1.0-preview-7ed2770ae
@metamask-previews/money-account-balance-service@2.2.0-preview-7ed2770ae
@metamask-previews/money-account-controller@0.3.3-preview-7ed2770ae
@metamask-previews/money-account-upgrade-controller@2.2.1-preview-7ed2770ae
@metamask-previews/multichain-account-service@13.0.0-preview-7ed2770ae
@metamask-previews/multichain-api-middleware@4.0.1-preview-7ed2770ae
@metamask-previews/multichain-network-controller@3.2.1-preview-7ed2770ae
@metamask-previews/multichain-transactions-controller@7.1.1-preview-7ed2770ae
@metamask-previews/name-controller@9.1.2-preview-7ed2770ae
@metamask-previews/network-connection-banner-controller@0.1.0-preview-7ed2770ae
@metamask-previews/network-controller@34.0.0-preview-7ed2770ae
@metamask-previews/network-enablement-controller@5.6.0-preview-7ed2770ae
@metamask-previews/notification-services-controller@25.0.0-preview-7ed2770ae
@metamask-previews/passkey-controller@2.1.0-preview-7ed2770ae
@metamask-previews/permission-controller@13.1.1-preview-7ed2770ae
@metamask-previews/permission-log-controller@5.1.0-preview-7ed2770ae
@metamask-previews/perps-controller@9.2.1-preview-7ed2770ae
@metamask-previews/phishing-controller@17.2.1-preview-7ed2770ae
@metamask-previews/platform-api-docs@0.0.0-preview-7ed2770ae
@metamask-previews/polling-controller@16.0.8-preview-7ed2770ae
@metamask-previews/preferences-controller@23.1.0-preview-7ed2770ae
@metamask-previews/profile-metrics-controller@4.0.2-preview-7ed2770ae
@metamask-previews/profile-sync-controller@28.3.0-preview-7ed2770ae
@metamask-previews/ramps-controller@17.0.0-preview-7ed2770ae
@metamask-previews/rate-limit-controller@7.0.1-preview-7ed2770ae
@metamask-previews/react-data-query@0.2.1-preview-7ed2770ae
@metamask-previews/remote-feature-flag-controller@4.2.2-preview-7ed2770ae
@metamask-previews/sample-controllers@5.0.3-preview-7ed2770ae
@metamask-previews/seedless-onboarding-controller@10.0.3-preview-7ed2770ae
@metamask-previews/selected-network-controller@26.1.5-preview-7ed2770ae
@metamask-previews/sentinel-api-service@1.0.0-preview-7ed2770ae
@metamask-previews/shield-controller@5.1.3-preview-7ed2770ae
@metamask-previews/signature-controller@39.2.7-preview-7ed2770ae
@metamask-previews/smart-transactions-controller@25.0.1-preview-7ed2770ae
@metamask-previews/snap-account-service@2.0.0-preview-7ed2770ae
@metamask-previews/social-controllers@2.5.0-preview-7ed2770ae
@metamask-previews/solana-test-validator-up@1.0.0-preview-7ed2770ae
@metamask-previews/stellar-quickstart-up@0.0.0-preview-7ed2770ae
@metamask-previews/storage-service@1.0.2-preview-7ed2770ae
@metamask-previews/subscription-controller@6.2.1-preview-7ed2770ae
@metamask-previews/transaction-controller@69.0.0-preview-7ed2770ae
@metamask-previews/transaction-pay-controller@25.0.0-preview-7ed2770ae
@metamask-previews/user-operation-controller@41.2.7-preview-7ed2770ae
@metamask-previews/wallet@7.0.1-preview-7ed2770ae
@metamask-previews/wallet-cli@0.0.0-preview-7ed2770ae

sirtimid added a commit to MetaMask/metamask-extension that referenced this pull request Jul 16, 2026
Construct the extension's GasFeeController through @metamask/wallet
instead of the local messenger-client-init wiring, integrating
MetaMask/core#9527.

The wallet now owns GasFeeController as an instanceOptions.gasFeeController
slot: it builds getProvider and getCurrentNetworkEIP1559Compatibility from
NetworkController itself and lets the controller default onNetworkDidChange
and getChainId via its messenger subscription. The extension supplies only
the client-specific overrides (interval, clientId, gas API endpoints, and
the BSC legacy gas API compatibility check).

- Add wallet-init/instance-options/gas-fee-controller.ts and a minimal
  wallet-init/messengers/gas-fee-controller-messenger.ts (delegates only
  NetworkController:getState + getNetworkClientById for the chain-id read).
- Wire gasFeeController into wallet-init/initialization.ts.
- Resolve this.gasFeeController via this.wallet.getInstance('GasFeeController')
  and drop the GasFeeControllerInit import + init-map entry.
- Delete the local confirmations/gas-fee-controller-init and
  messengers/gas-fee-controller-messenger (+tests) and their entries in
  messenger-client-init/messengers/index.ts. GasFeeController stays in
  controller-list.ts.

Preview-pin @metamask/wallet and @metamask/gas-fee-controller to the
core#9527 preview builds (7ed2770ae) until a wallet release ships it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sirtimid
sirtimid marked this pull request as ready for review July 16, 2026 15:14
@sirtimid
sirtimid requested a review from a team as a code owner July 16, 2026 15:14
@sirtimid
sirtimid temporarily deployed to default-branch July 16, 2026 15:18 — with GitHub Actions Inactive
sirtimid added a commit to MetaMask/metamask-extension that referenced this pull request Jul 16, 2026
Construct the extension's GasFeeController through @metamask/wallet
instead of the local messenger-client-init wiring, integrating
MetaMask/core#9527.

The wallet now owns GasFeeController as an instanceOptions.gasFeeController
slot: it builds getProvider and getCurrentNetworkEIP1559Compatibility from
NetworkController itself and lets the controller default onNetworkDidChange
and getChainId via its messenger subscription. The extension supplies only
the client-specific overrides (interval, clientId, gas API endpoints, and
the BSC legacy gas API compatibility check).

- Add wallet-init/instance-options/gas-fee-controller.ts and a minimal
  wallet-init/messengers/gas-fee-controller-messenger.ts (delegates only
  NetworkController:getState + getNetworkClientById for the chain-id read).
- Wire gasFeeController into wallet-init/initialization.ts.
- Resolve this.gasFeeController via this.wallet.getInstance('GasFeeController')
  and drop the GasFeeControllerInit import + init-map entry.
- Delete the local confirmations/gas-fee-controller-init and
  messengers/gas-fee-controller-messenger (+tests) and their entries in
  messenger-client-init/messengers/index.ts. GasFeeController stays in
  controller-list.ts.

Preview-pin @metamask/wallet and @metamask/gas-fee-controller to the
core#9527 preview builds (7ed2770ae) until a wallet release ships it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@rekmarks rekmarks left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Just a couple of things.

Comment thread packages/wallet/src/initialization/initialization.ts Outdated
sirtimid added a commit to MetaMask/metamask-extension that referenced this pull request Jul 20, 2026
Construct the extension's GasFeeController through @metamask/wallet
instead of the local messenger-client-init wiring, integrating
MetaMask/core#9527.

The wallet now owns GasFeeController as an instanceOptions.gasFeeController
slot: it builds getProvider and getCurrentNetworkEIP1559Compatibility from
NetworkController itself and lets the controller default onNetworkDidChange
and getChainId via its messenger subscription. The extension supplies only
the client-specific overrides (interval, clientId, gas API endpoints, and
the BSC legacy gas API compatibility check).

- Add wallet-init/instance-options/gas-fee-controller.ts and a minimal
  wallet-init/messengers/gas-fee-controller-messenger.ts (delegates only
  NetworkController:getState + getNetworkClientById for the chain-id read).
- Wire gasFeeController into wallet-init/initialization.ts.
- Resolve this.gasFeeController via this.wallet.getInstance('GasFeeController')
  and drop the GasFeeControllerInit import + init-map entry.
- Delete the local confirmations/gas-fee-controller-init and
  messengers/gas-fee-controller-messenger (+tests) and their entries in
  messenger-client-init/messengers/index.ts. GasFeeController stays in
  controller-list.ts.

Preview-pin @metamask/wallet and @metamask/gas-fee-controller to the
core#9527 preview builds (7ed2770ae) until a wallet release ships it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sirtimid added a commit that referenced this pull request Jul 20, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sirtimid added a commit that referenced this pull request Jul 20, 2026
Address review feedback on #9527:

- Make `instanceOptions.gasFeeController.clientId` required and drop the
  `'cli'` default, so every client (extension, mobile, wallet-cli) must
  identify itself to the gas API rather than silently inheriting a
  default. `gasFeeController` is now a required instance-options key
  (mirroring `networkController`); wallet-cli passes `clientId: 'cli'`
  explicitly.
- Throw a descriptive error naming the members of the cycle when
  `orderByDependencies` cannot satisfy the declared dependencies,
  instead of silently deferring to a later "handler not registered"
  messenger error.

Also re-add the `gas-fee-controller` instance rule via the new
programmatic `codeowners.ts` generator (#9529) picked up in the rebase
onto main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sirtimid
sirtimid force-pushed the sirtimid/wire-gas-fee-controller branch from 7ed2770 to ec8eeec Compare July 20, 2026 14:27
@sirtimid
sirtimid requested a review from a team as a code owner July 20, 2026 14:27
* @returns The configurations ordered so that dependencies come first.
* @throws If the configurations contain a dependency cycle.
*/
export function orderByDependencies(

@matthewwalsh0 matthewwalsh0 Jul 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is technically very cool! But logistically terrifying 😅

Should we ensure we don't migrate packages until they are initialisation-order-agnostic?

We had to do this for TransactionController for example.

I'd hope these dependencies are very rare, as we usually rely on legacy callbacks or dynamic messenger calls, rather than hard dependencies at constructor / initialisation time?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we ensure we don't migrate packages until they are initialisation-order-agnostic?

We'll look into what it'd take to do this, but are we not already doing the equivalent of this in the clients?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matthewwalsh0 here is the controller change #9569

@matthewwalsh0
matthewwalsh0 self-requested a review July 20, 2026 14:35
@rekmarks
rekmarks dismissed their stale review July 20, 2026 16:25

My review was addressed, holding approval pending resolution of other feedback.

pull Bot pushed a commit to dmrazzy/core that referenced this pull request Jul 21, 2026
…gnostic (MetaMask#9569)

## Explanation

The `GasFeeController` constructor was the only
currently-wired-or-wireable controller that performed **eager,
construction-time reads** of `NetworkController` and the network
provider:

- `new EthQuery(this.#getProvider())` — unconditional.
- Seeding `currentChainId` — either via an eager `getChainId()` call or
via `NetworkController:getState` + `getNetworkClientById`.

This forced any shared initialization (e.g. `@metamask/wallet`) to
guarantee `NetworkController` is constructed *before*
`GasFeeController`, which is exactly the construction-order coupling we
want to avoid. Every other order-agnostic controller resolves its
collaborators lazily.

This PR defers those reads to first use so the controller can be
constructed in any order, matching the pattern already used elsewhere.
**No public API change** — the constructor signature and
gas-fee-fetching behavior are unchanged.

## Changes

- Removed the eager `EthQuery` build and the eager `currentChainId`
seeding from the constructor. Network-change subscription wiring is kept
as-is (subscribing is safe regardless of construction order).
- Added lazy, memoized resolution:
- `#getEthQuery()` — builds the `EthQuery` from `getProvider` on first
use.
- `#getCurrentChainId()` — resolves the chain ID from the `getChainId`
callback when provided (only when paired with `onNetworkDidChange`,
preserving the original coupling), otherwise from `NetworkController`.
- `#onNetworkControllerDidChange` now resets `ethQuery` to `undefined`
(rebuilt lazily on the next fetch) instead of rebuilding it eagerly per
network change, and still updates `currentChainId`.

## Testing

- New tests assert construction performs **no**
`NetworkController:getState` / `getNetworkClientById` / `getProvider` /
`getChainId` calls — even when those handlers throw — for both the
callback and no-callback constructor branches.
- New test covers network-change handling via both the
`onNetworkDidChange` callback and the
`NetworkController:networkDidChange` messenger subscription (chain ID +
eth query update on the next fetch).
- All existing tests pass unchanged (53 total). 100% coverage on the
changed lines; remaining uncovered lines are pre-existing.

## References

Splits out the root-cause fix from the wiring PR MetaMask#9527, so that
`GasFeeController` can be migrated into the shared `@metamask/wallet`
init set without a dependency-ordering mechanism.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches core gas-estimation initialization and network-switch caching;
behavior is intended to stay the same but timing of first network reads
shifts to first fetch.
> 
> **Overview**
> **`GasFeeController` no longer touches `NetworkController` or the RPC
provider during construction**, so it can be instantiated before the
network stack is ready (e.g. shared `@metamask/wallet` init) without
changing the public constructor or fetch semantics.
> 
> The constructor drops eager `EthQuery` creation and immediate chain-ID
resolution (`getChainId()` / `getState` + `getNetworkClientById`).
Instead, **`#getEthQuery()`** and **`#getCurrentChainId()`** memoize
provider and chain ID on first use inside
**`_fetchGasFeeEstimateData`**. When **`onNetworkDidChange` +
`getChainId`** are supplied, the callback is stored without calling
`getChainId` at construct time.
> 
> On network change, **`#onNetworkControllerDidChange`** clears the
cached `ethQuery` (rebuilt on the next fetch) and still updates
`currentChainId` and polling. Tests lock in zero constructor-time
network/provider/`getChainId` calls and correct chain ID + provider
refresh after network switches via callback or
**`NetworkController:networkDidChange`**.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
66c249d. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sirtimid
sirtimid force-pushed the sirtimid/wire-gas-fee-controller branch from ec8eeec to ee084b8 Compare July 21, 2026 18:09
Wire `GasFeeController` into `@metamask/wallet`'s default initialization
set as its own `InitializationConfiguration`, mirroring the
`transaction-controller` / `network-controller` instances. A wired
`TransactionController` delegates `GasFeeController:fetchGasFeeEstimates`
lazily, so the wallet boots today but throws the first time a transaction
flow needs gas estimates; wiring `GasFeeController` supplies that handler.

Adds a required `instanceOptions.gasFeeController` option whose `clientId`
(sent as `X-Client-Id` to the gas API) is required, so every client
identifies itself; all other fields are optional and fall back to
platform-agnostic defaults. wallet-cli passes `clientId: 'cli'`.

The instance builds `getProvider` / `getCurrentNetworkEIP1559Compatibility`
as lazy closures over `NetworkController`, and `GasFeeController` was made
initialization-order-agnostic upstream (#9569), so no dependency-ordering
machinery is needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sirtimid
sirtimid force-pushed the sirtimid/wire-gas-fee-controller branch from ee084b8 to baa4472 Compare July 21, 2026 18:57
@sirtimid
sirtimid requested a review from rekmarks July 21, 2026 19:15

@rekmarks rekmarks left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@sirtimid
sirtimid added this pull request to the merge queue Jul 22, 2026
Merged via the queue into main with commit 7a956ca Jul 22, 2026
825 of 846 checks passed
@sirtimid
sirtimid deleted the sirtimid/wire-gas-fee-controller branch July 22, 2026 15:25
pull Bot pushed a commit to dmrazzy/core that referenced this pull request Jul 24, 2026
…etaMask#9612)

## Explanation

Makes the `@metamask/wallet-cli` daemon able to process a transaction
end-to-end by completing the two consumer-side pieces a wired
`TransactionController` needs at runtime. This makes the daemon
transaction-*capable*; the user-facing `mm send` command is a separate
follow-on.

`TransactionController` is wired into `@metamask/wallet` and its
messenger delegates to `GasFeeController` and `ApprovalController`. A
send flow currently dead-ends in the daemon in two ways, both fixed
here.

### Piece 1 — consume `GasFeeController`

The `gasFeeController` slot (`clientId: 'cli'`) already exists in
`buildInstanceOptions` — it was added when `GasFeeController` was wired
upstream (MetaMask#9527) because `clientId` is required. This PR finishes the
consumer side:

- Documents the slot in the `buildInstanceOptions` JSDoc slot list.
- Relies on the wallet package's platform-agnostic **production
default** for `EIP1559APIEndpoint` (the default is already the prod URL)
rather than re-specifying the string in the CLI, so the endpoint stays
centrally owned.

With `GasFeeController` wired and released, a daemon-hosted `Wallet` now
resolves `GasFeeController:fetchGasFeeEstimates` instead of throwing `A
handler for ... has not been registered`.

### Piece 2 — headless auto-approval

`ApprovalController:addRequest` is **awaited** by transaction/signature
flows. The daemon's `showApprovalRequest` is a no-op (it only signals "a
request needs attention"; it does not resolve anything), so with no UI
the awaiting call hangs forever.

`subscribeToAutoApproval` subscribes to
`ApprovalController:stateChanged` and immediately accepts every pending
request via `ApprovalController:acceptRequest`. The
`showApprovalRequest` hook stays a no-op (there is no UI); the id isn't
available to that hook, so acceptance goes through the messenger
instead. An `inFlight` guard keeps accepting idempotent across the
re-entrant/rapid state changes a single flow emits, and both sync throws
and async rejections from an accept are logged and swallowed so one bad
request can't crash the daemon or wedge the subscription. The
subscription is installed in `createWallet` and torn down in its
`dispose` path.

### Security consideration — auto-approval is a conscious trust decision

Auto-approval means the daemon **accepts every approval request without
confirmation** — transactions and signatures included. For a headless
daemon this is the intended model: it is driven only by its owner's
local CLI over a `0600`, same-user Unix socket, so the trust boundary is
the socket, not a per-request prompt. This is documented as the daemon's
explicit trust model in `subscribeToAutoApproval` and the README, and
flagged as **not "safe by default"** — a scoped/opt-in policy (config
flag, or accepting only specific approval types) is deferred until the
user-facing send command exists.

## References

- `packages/wallet-cli/src/daemon/auto-approval.ts` — the auto-approval
subscription + trust model.
- `packages/wallet-cli/src/daemon/wallet-factory.ts` —
`buildInstanceOptions` slot docs; `createWallet`/`teardown` wiring.

## Related

- Closes MetaMask#9512
- Builds on MetaMask#9509 (TransactionController slot) and MetaMask#9527 (wire
`GasFeeController` into `@metamask/wallet`)
- Unblocks MetaMask#9513 (the `mm` send command), which is additionally gated on
MetaMask#9511

## Checklist

- [x] Tests cover both the gas slot and the auto-approval accept path
(unit + real-`Wallet` integration); 100% coverage maintained.
- [x] `build`, package `test`, `yarn lint:fix`, `yarn lint`,
`changelog:validate` pass.
- [x] Auto-approval trust model documented in code and README.
- [x] Teardown (`dispose`) unsubscribes the auto-approval listener.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **High Risk**
> The daemon unconditionally approves transactions and signatures with
no per-request prompt; compromise of the local Unix socket trust
boundary can move funds.
> 
> **Overview**
> Enables end-to-end transaction capability in the headless wallet
daemon by **auto-accepting every pending `ApprovalController` request**
via a new `subscribeToAutoApproval` subscription on
`ApprovalController:stateChanged`, so awaited `addRequest` calls no
longer hang behind the no-op `showApprovalRequest` hook. Acceptance uses
an **in-flight guard** and logs/swallows accept failures so re-entrant
state updates and bad requests cannot wedge or crash the daemon.
> 
> `createWallet` installs the subscription before `wallet.init` and
tears it down in `dispose` (shared `runUnsubscribe` helper alongside
persistence). **README and changelog** document the explicit security
model: anything on the `0600` same-user socket can move funds; scoped
approval policy is deferred.
> 
> Also expands **`buildInstanceOptions` JSDoc** for the existing
`gasFeeController` slot (`clientId: 'cli'`, relying on wallet defaults
for the production gas API URL).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
26e4183. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pull Bot pushed a commit to Pe44e/metamask-mobile that referenced this pull request Jul 29, 2026
…metamask/wallet (MetaMask#33430)

## **Description**

Bumps `@metamask/wallet` to **v8.1.0** and adopts the two controllers it
newly wires into the default `Wallet` initialization set, continuing the
shared controller-integration migration
([`@metamask/wallet`](https://github.com/MetaMask/core/tree/main/packages/wallet)
is the layer that extension, mobile, and wallet-cli all share).

**GasFeeController** (core
[MetaMask#9527](MetaMask/core#9527)):
- Adds `wallet-init/instance-options/gas-fee-controller.ts` (+ test)
supplying the mobile-specific options: `clientId`, EIP-1559/legacy gas
API endpoints, and `getCurrentNetworkLegacyGasAPICompatibility` (mainnet
‖ BSC ‖ Polygon). The wallet builds `getProvider` and
`getCurrentNetworkEIP1559Compatibility` from `NetworkController` itself.
- Deletes the local `gas-fee-controller-init` and
`gas-fee-controller-messenger` (+ tests + barrels).
- Resolves the instance via
`this.#wallet.getInstance('GasFeeController')` in `Engine.ts`.

**SeedlessOnboardingController** (core
[MetaMask#9533](MetaMask/core#9533)) — required in the
same PR because wallet@8 wires both; leaving the local init would cause
a double-registration boot crash:
- Adds `wallet-init/instance-options/seedless-onboarding-controller.ts`
(+ test) supplying the mobile-specific options: encryptor
`cipher`↔`data` adapter, JWT handlers via `AuthTokenHandler`,
`web3AuthNetwork`, and `passwordOutdatedCacheTTL`.
- Deletes the local `seedless-onboarding-controller-messenger` factory
entry from `MESSENGER_FACTORIES`.
- Updates E2E Metro mock to intercept
`@metamask/seedless-onboarding-controller` at the **package** level
(routing to `tests/module-mocking/seedless/package.ts`) instead of the
former local controller path.
- Resolves the instance via
`this.#wallet.getInstance('SeedlessOnboardingController')` in
`Engine.ts`.

Both controllers are dropped from `MessengerClientsToInitialize` only;
they remain in `MessengerClients` / `EngineState` / actions / events /
`BACKGROUND_STATE_CHANGE_EVENT_NAMES`, matching the already-migrated
controllers.

## **Changelog**

CHANGELOG entry: null

## **Related issues**

Refs: MetaMask/core#9527
Refs: MetaMask/core#9533

## **Manual testing steps**

1. Build and launch the app on iOS or Android and confirm it boots with
no `A handler for GasFeeController:* has already been registered` or
`SeedlessOnboardingController:*` error.
2. Open the send/confirmation flow on an EIP-1559 network (e.g. Ethereum
mainnet) and confirm gas fee estimates (low / market / aggressive) load
and update.
3. Repeat on a legacy-gas network (e.g. BSC or Polygon) and confirm gas
prices load.
4. Submit a transaction and confirm gas estimation drives the fee
correctly.
5. On a build with seedless onboarding enabled, complete a social login
flow and confirm vault encryption/decryption works end-to-end.

## **Screenshots/Recordings**

N/A — internal controller-wiring refactor; no UI changes.

### **Before**

<!-- N/A -->

### **After**

<!-- N/A -->

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

#### Performance checks (if applicable)

- [x] I've tested on Android
  - Ideally on a mid-range device; emulator is acceptable
- [x] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [x] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example

For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **High Risk**
> Touches gas estimation for sends/confirmations and seedless vault
encryption/onboarding—both security- and funds-sensitive—and changes
controller lifecycle in a way that can cause boot failures if
registration diverges.
> 
> **Overview**
> Bumps **`@metamask/wallet`** to **v8.1.0** (with
**`@metamask/gas-fee-controller`** and
**`@metamask/seedless-onboarding-controller`** bumps) and moves
**GasFeeController** and **SeedlessOnboardingController** off mobile’s
Engine messenger-client init path into the shared Wallet bootstrap.
> 
> **GasFeeController** and **SeedlessOnboardingController** are no
longer registered via local `*-init` modules or messenger factories.
Mobile passes **`gasFeeController`** and
**`seedlessOnboardingController`** **`instanceOptions`** from new
**`wallet-init/instance-options/*`** builders (gas API endpoints,
legacy-gas compatibility, seedless encryptor adapter, JWT handlers,
Web3Auth network). **`Engine`** resolves both with
**`this.#wallet.getInstance(...)`** instead of
**`messengerClientsByName`**, avoiding double registration at boot.
> 
> Local **`gas-fee-controller-init`**,
**`gas-fee-controller-messenger`**, and the old seedless init/messenger
wiring are removed; tests move to the instance-option modules.
**CODEOWNERS** and E2E **Metro** mocks shift to
**`@metamask/seedless-onboarding-controller`** at the package level
because Wallet now constructs that controller.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
82f1fe5. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Erik Marks <25517051+rekmarks@users.noreply.github.com>
roz0n pushed a commit to MetaMask/metamask-mobile that referenced this pull request Jul 29, 2026
…metamask/wallet (#33430)

## **Description**

Bumps `@metamask/wallet` to **v8.1.0** and adopts the two controllers it
newly wires into the default `Wallet` initialization set, continuing the
shared controller-integration migration
([`@metamask/wallet`](https://github.com/MetaMask/core/tree/main/packages/wallet)
is the layer that extension, mobile, and wallet-cli all share).

**GasFeeController** (core
[#9527](MetaMask/core#9527)):
- Adds `wallet-init/instance-options/gas-fee-controller.ts` (+ test)
supplying the mobile-specific options: `clientId`, EIP-1559/legacy gas
API endpoints, and `getCurrentNetworkLegacyGasAPICompatibility` (mainnet
‖ BSC ‖ Polygon). The wallet builds `getProvider` and
`getCurrentNetworkEIP1559Compatibility` from `NetworkController` itself.
- Deletes the local `gas-fee-controller-init` and
`gas-fee-controller-messenger` (+ tests + barrels).
- Resolves the instance via
`this.#wallet.getInstance('GasFeeController')` in `Engine.ts`.

**SeedlessOnboardingController** (core
[#9533](MetaMask/core#9533)) — required in the
same PR because wallet@8 wires both; leaving the local init would cause
a double-registration boot crash:
- Adds `wallet-init/instance-options/seedless-onboarding-controller.ts`
(+ test) supplying the mobile-specific options: encryptor
`cipher`↔`data` adapter, JWT handlers via `AuthTokenHandler`,
`web3AuthNetwork`, and `passwordOutdatedCacheTTL`.
- Deletes the local `seedless-onboarding-controller-messenger` factory
entry from `MESSENGER_FACTORIES`.
- Updates E2E Metro mock to intercept
`@metamask/seedless-onboarding-controller` at the **package** level
(routing to `tests/module-mocking/seedless/package.ts`) instead of the
former local controller path.
- Resolves the instance via
`this.#wallet.getInstance('SeedlessOnboardingController')` in
`Engine.ts`.

Both controllers are dropped from `MessengerClientsToInitialize` only;
they remain in `MessengerClients` / `EngineState` / actions / events /
`BACKGROUND_STATE_CHANGE_EVENT_NAMES`, matching the already-migrated
controllers.

## **Changelog**

CHANGELOG entry: null

## **Related issues**

Refs: MetaMask/core#9527
Refs: MetaMask/core#9533

## **Manual testing steps**

1. Build and launch the app on iOS or Android and confirm it boots with
no `A handler for GasFeeController:* has already been registered` or
`SeedlessOnboardingController:*` error.
2. Open the send/confirmation flow on an EIP-1559 network (e.g. Ethereum
mainnet) and confirm gas fee estimates (low / market / aggressive) load
and update.
3. Repeat on a legacy-gas network (e.g. BSC or Polygon) and confirm gas
prices load.
4. Submit a transaction and confirm gas estimation drives the fee
correctly.
5. On a build with seedless onboarding enabled, complete a social login
flow and confirm vault encryption/decryption works end-to-end.

## **Screenshots/Recordings**

N/A — internal controller-wiring refactor; no UI changes.

### **Before**

<!-- N/A -->

### **After**

<!-- N/A -->

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

#### Performance checks (if applicable)

- [x] I've tested on Android
  - Ideally on a mid-range device; emulator is acceptable
- [x] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [x] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example

For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **High Risk**
> Touches gas estimation for sends/confirmations and seedless vault
encryption/onboarding—both security- and funds-sensitive—and changes
controller lifecycle in a way that can cause boot failures if
registration diverges.
> 
> **Overview**
> Bumps **`@metamask/wallet`** to **v8.1.0** (with
**`@metamask/gas-fee-controller`** and
**`@metamask/seedless-onboarding-controller`** bumps) and moves
**GasFeeController** and **SeedlessOnboardingController** off mobile’s
Engine messenger-client init path into the shared Wallet bootstrap.
> 
> **GasFeeController** and **SeedlessOnboardingController** are no
longer registered via local `*-init` modules or messenger factories.
Mobile passes **`gasFeeController`** and
**`seedlessOnboardingController`** **`instanceOptions`** from new
**`wallet-init/instance-options/*`** builders (gas API endpoints,
legacy-gas compatibility, seedless encryptor adapter, JWT handlers,
Web3Auth network). **`Engine`** resolves both with
**`this.#wallet.getInstance(...)`** instead of
**`messengerClientsByName`**, avoiding double registration at boot.
> 
> Local **`gas-fee-controller-init`**,
**`gas-fee-controller-messenger`**, and the old seedless init/messenger
wiring are removed; tests move to the instance-option modules.
**CODEOWNERS** and E2E **Metro** mocks shift to
**`@metamask/seedless-onboarding-controller`** at the package level
because Wallet now constructs that controller.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
82f1fe5. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Erik Marks <25517051+rekmarks@users.noreply.github.com>
sirtimid added a commit that referenced this pull request Aug 3, 2026
Rebasing #9300 onto `main` after five weeks of drift required these
adjustments, none of which change the feature being wired:

- `codeowners.ts` is now the source of truth for `.github/CODEOWNERS`,
  which is generated. `initializationPath` accepts an array so
  `permission-controller` can claim both of the instance directories it
  supplies, and rules are sorted by pattern so the generated section stays
  alphabetical.
- `Wallet` tests pass the `gasFeeController` instance option, required
  since #9527.
- Regenerate `README.md` and both `tsconfig`s via their fixers.
- Repair the `[6.0.0]` changelog section, which a conflict resolution had
  left with duplicated content.
- Spell out the client-facing consequences of the wiring in the changelog:
  the duplicate-registration collision for consumers passing their own
  messenger, and the fact that permission specifications with side effects
  (the Snaps specifications reach `SnapController:*`) need a wider
  delegation allowlist than the default configuration provides.
- Cover `caveatSpecifications` injection, the one instance option without a
  behavioural assertion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wire GasFeeController into @metamask/wallet default initialization

3 participants